7.1 项目结构
多文件Flask程序的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| |-flasky |-app/ |-init__.py |-templates/ # 模板 |-static/ # 静态文件 |-main/ # 蓝本,名字可更改 |-__init__.py |-errors.py # 路由,视图函数 |-forms.py # 表单 |-views.py # 路由器,视图函数 |-email.py # 邮件支持 |-models.py # 数据库模型 |-migrations # 数据库迁移 |-tests/ # 单元测试 |-__init__.py |-test*.py |-venv/ |-requirements.txt # 依赖包文本 |-config.py # 配置 |-manage.py # 启动程序
|
7.2 配置config.py
在前面的章节中,我们在hello.py
中是使用字典状结构配置的(如app.config['FLASKY_ADMIN'] = 12345678@qq.com
),现在我们把相关配置提取出来,在config.py
中使用层次结构的配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import os basedir = os.path.abspath(os.path.dirname(__file__)) # 设置通用配置 class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' SQLALCHEMY_COMMIT_ON_TEARDOWN = True FLASK_MAIL-SUBJECT_PREFECT = '[Flasky]' FLASKY_MAIL_SENDER = 'Flasky Admin 123456789@qq.com' FLASK_ADMIN = os.environ.get('FLASKY_ADMIN') @staticmethod # init_app()可对当前环境的配置初始化,参数是程序实例 def init_app(app): pass # 开发环境配置(继承Config基类) class DevelopmentConfig(Config): DEBUG = True MAIL_SERVER = 'smtp.qq.com' MAIL_PORT = 465 MAIL_USE_SSL = True MAIL_USENAME = os.environ.get('MAIL_USENAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') SQLALCHEMY_DATABASE_URI = os.environ,get('DEV_DATABASE_URI') or \'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') # 测试环境配置(继承Config基类) class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = os.environ,get('TEST_DATABASE_URI') or \'sqlite:///' + os.path.join(basedir, 'data-test.sqlite') # 生产环境配置(继承Config基类) class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ,get('DATABASE_URI') or \'sqlite:///' + os.path.join(basedir, 'data.sqlite') # 配置字典 config = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, 'default': DevelopmentConfig }
|
7.3 程序包(app文件夹)
程序包用来保存程序的所有代码、模板和静态文件。
7.3.1 使用程序工厂函数
存在问题:在单脚本中开发程序的缺点是:脚步在运行时,程序实例已经创建,已经不能修改配置,自然也无法动态修改配置。这样也不利于单元测试。
解决思路:延迟创建程序实例。
解决方法:把创建过程移到可显式调用的工厂函数中。
这样不仅可以给脚步流出配置程序时间,还能创建多个程序实例。
程序的工厂函数在app包的构造文件(__init__.py
)中定义。
app/__inti__.py
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| from flask import Flask from flask_bootstrap import Bootstrap from flask_mail import Mail from flask_moment import Moment from flask_sqlalchemy import SQLAlchemy from config import config bootstrap = Bootstrap() mail = Mail() moment = Moment() db = SQLAlchemy() # 定义工厂函数,参数为程序使用的配置环境名 def create_app(config_name): app = Flask(__name__) # 根据配置环境名获取对应的配置类从而获取相应的配置(类变量) app.config.from_object(config['config_name'] # 使用配置类中的init_app()方法初始化配置 config['config_name'].init_app(app) bootstrap.init_app(app) mail.init_app(app) moment.init_app(app) db.init_app(app) # 附加路由和自定义的错误页面 return app
|
config.py
文件中定义的配置类,可以使用app.config
配置对象提供的from_object()
方法直接导入程序。
程序创建(app=Flask(__name__)
)并配置好后,就能初始化Flask拓展(在Flask拓展实例对象上调用init_app()
方法,如bootstrap.init(app)
)
工厂函数返回的是程序实例,不过此时工厂函数创建的程序实例还不完整,因为还没有路由和自定义的错误页面处理程序(简单来说就是缺少路由)。
7.3.2 在蓝本中实现程序功能
存在问题:在单脚本程序中,程序实例存在于全局作用域中,路由可以直接使用app.route
修饰器定义,但是现在程序实例时在运行时创建的(调用create_app()
函数),这时定义路由已经太晚了(因为只有调用create_app()
函数创建实例后才能使用app.route
修饰器,而路由又要定义在create_app()
函数里)
解决方法:使用蓝本。蓝本和程序类似,也可以定义路由,但不同的是:在蓝本中定义的路由会处于休眠状态,直到调用app.register_blueprint()
方法把蓝本注册到程序后,路由在真正称为程序的一部分。
理解蓝本:蓝本通常作用于相同的URL前缀,如user/id
、user/profile
这样的地址,都是以/user
开头,它们是一组用户相关的操作,那么就可以放在一个模块中。大多数项目都是把蓝本当做拆分视图用的。
使用位于全局作用域中的蓝本时,定义路由方法和单脚本程序基本一样(不同之处下面会讲到)
- 创建蓝本
app/main/__init__.py
:1 2 3 4 5 6
| from flask import Blueprint main = Blueprint('main', __name__) # 导入view和error模块,导入之后就能将路由和错误处理程序与蓝本关联起来 from . import views, errors
|
Blueprint()
类的第一个参数是蓝本名字,第二个参数是篮本所在的包或模块(一般使用__name__
即可)。
注意: 像view
和error
这些模块,为了避免循环导入,要在app/main/__init.py__
的末尾处导入,因为在view.py
和error.py
中还要导入蓝本main
。
1 2 3 4 5 6 7 8 9 10
| from flask import render_template from . import main @main.app_errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @main.app_errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500
|
注意:在蓝本中编写错误处理程序稍有不同,如果是使用errorhandler
修饰器,那么只有蓝本中的错误才能触发处理程序。因此要想注册程序全局的错误处理程序,必须使用app_errorhandler
修饰器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from datetime import datetime from flask import render_template, session, redirect, url_for, current_app from . import main from .froms import NameForm from .. import db from ..models import User @main.route('/', methods=['GET', 'POST']) def index(): form = NmaeForm() if form.validate_on_submit(): # ... return redirect(url_for('.index') return render_template('index.html', form=form, name=session.get('name'), know=session.get('know', Flase), current_time=datetime.utcnow())
|
注意:在蓝本中编写视图函数主要有两点不同:(1)和前面的错误处理程序一样,路由修饰器由蓝本提供(体现在main.route()
,而不是app.route()
)。
(2)url_for()
函数的第一个参数。url_for()
函数的第一个参数时路由的端点名,在程序中默认为视图函数的名字(所以在单脚本中可以url_for('index')
),但是在蓝本中,Flask会为蓝本中的全部端点加上一个命名空间(也就是蓝本的名字),这样做是为了可以在不同的蓝本中使用相同的端点名(函数名)定义视图函数。所以该蓝本中视图函数index()
注册的端点名是main.index
,因此需要使用url_for('main.index')
,也可以简写为url_for('.index')
(前提是同一蓝本,跨蓝本的话必须使用带有命名空间的端点名)。
另外:模板中的hrfe
属性中的链接,使用url_for()
获取的,其中参数也是同样写端点名。
重点注意:如果在其他地方(如这里的app/main/view.py
、app/email.py
等)需要用到程序实例app
,均需使用from flask import current_app
来导入程序上下文。
2.在工厂函数create_app()
中把蓝本注册到程序上,app/__init__.py
如下:
1 2 3 4 5 6 7 8 9 10
| # ... def create_app(config_name): # ... # 导入整个包 from .main import main as main_blueprint app.register_blueprint(main_blueprint) return app
|
7.4 启动脚本
manage.py
用于启动脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #!/usr/bin/env python import os from app import create_app, db from app.models import User, Role from flask_script import Manager, Shell from flask_migrate import Migrate, MigrateCommand # 获取配置名,并创建程序 app = create_app(os.getenv('FLASK_CONFIG') or 'default') manager = Manager(app) migrate = Migrate(app, db) def make_shell_context(): return dict(app=app, db=db, User=User, Role=Role) manager.add_command('shell', Shell(make_context=make_shell_context)) manager.add_command('db', MigrateCommand) if __name__ == '__main__': manager.run()
|
在脚本中加入了#!/usr/bin/env python
声明,因此在Unix系统中可以直接通过./manage.py
命令运行脚步,而无需使用python manage.py
命令。
7.5 需求文件
requirements.txt
文件:用于记录所有依赖包及其精确的版本号。可以使用如下命令自动生成这个文件:
(venv) $ pip freeze > requirements.txt
当要创建同一个环境时,可以使用如下命令:
(venv) $ pip install -r requirements.txt
7.6 单元测试
tests/test_basics.py
文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import unittest from flask import current_app from app import create_app, db class BasicsTestCase(unittest.TestCase): # 创建一个测试环境,并激活程序上下文,并创建数据库 def setUp(self): self.app = create_app('testing') self.app_context = self.app.app_context() self.app_context.push() db.create_all() # 删除数据库、程序上下文 def tearDown(self): db.session.remove() db.drop_all() self.app_context.pop() # 测试程序实例是否存在 def test_app_exists(self): self.assertFalse(current_app is None) # 测试程序的环境是否为TESTING def test_app_is_testing(self): self.assertTrue(current_app.config['TESTING'])
|
setUp()
和tearDown()
方法分别在各测试前后运行。函数名字以test_
开头的函数都作为测试执行。
- 运行单元测试。可在
manage.py
文件中添加一个自定义命令,用于执行测试:
1 2 3 4 5 6 7 8 9
| # ... @manager.command def test(): # 字符串内容会显示在帮助消息中 """Run the unit tests""" import unittest tests = unittest.TestLoader().discover('tests') unittest.TextRunner(verbosity=2).run(tests)
|
manager.command
修饰器修饰的函数名就是命令名。因此可以使用如下命令运行测试:
1 2 3 4 5 6 7 8
| (venv) $ python manage.py test test_app_exits (test_basics.BasicsTestCase) ... ok test_app_is_testing (test_basics.BasicsTestCase) ... ok -------------------------------------------------------- Ran 2 tests in 0.001s OK
|
7.7 创建数据库
首先需要在环境变量中定义所需要的数据库URI,然后再创建数据库。
不管从哪里获取数据库URI,都要在新数据库中创建数据表,如果使用Flask-Migrate跟踪迁移,可以使用如下命令创建数据表或者更新到最新版本:
(venv) $ python manage.py db upgrade